/*
 * @(#)Constraint.java        1.0 19-Apr-2009
 *
 * Copyright 2008 Thomas Ford. All rights reserved.
 */


package edu.napier.soc.xfdm.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * The {@code Constraint} class validates values belonging to an entity type
 * according to some constraint type (e.g. fixed, total or unique). More than
 * one function can be constrained but only one entity type can be involved.
 * <p/>
 * <p/>
 * Note: This class is immutable and therefore <strong>thread-safe</strong>.
 *
 * @author Thomas Ford
 * @version 1.0, 17-Apr-2009
 */
public class Constraint implements Serializable {

	private static final long serialVersionUID = 34535352495674l;

	private final String name;
	private final ConstraintType constraintType;
	private final EntityType entityType;
	private final List<Function> functions = new ArrayList<Function>();

	/**
	 * Constructs a new {@code Constraint} object.
	 *
	 * @param name           the name of the constraint.
	 * @param constraintType the constraint type.
	 * @param entityType     the entity type it is linked to.
	 * @param functions      the functions which are to be constrained.
	 */
	public Constraint(String name, ConstraintType constraintType,
	                  EntityType entityType, Set<Function> functions) {
		if (name == null) {
			throw new NullPointerException("name must not be null");
		}
		if (constraintType == null) {
			throw new NullPointerException("constraintType must not be null");
		}
		if (entityType == null) {
			throw new NullPointerException("entityType must not be null");
		}
		if (functions == null) {
			throw new NullPointerException("functions must not be null");
		}
		this.name = name;
		this.constraintType = constraintType;
		this.entityType = entityType;

		for (Function a : functions) {
			if (a != null) {
				this.functions.add(a);
			}
		}
	}

	private void validateTotal(List<Entity> records)
			throws ConstraintException {
		for (Entity e : records) {
			for (Function f : functions) {
				if (e.getValue(f) == null) {
					throw new ConstraintException(this, records.indexOf(e));
				}
			}
		}
	}

	/**
	 * @param f the function to be checked if it is part of this
	 *          {@code Constraint}.
	 * @return {@code true} if the function is in this {@code Constraint},
	 *         otherwise {@code false}.
	 */
	public boolean containsFunction(Function f) {
		return functions.contains(f);
	}

	private void validateTotal(Entity e) throws ConstraintException {
		for (Function f : functions) {
			if (e.getValue(f) == null) {
				throw new ConstraintException(this, 0);
			}
		}
	}

	/**
	 * @return the entity type associated with this {@code Constraint}.
	 */
	public EntityType getEntityType() {
		return entityType;
	}

	private void validateUnique(List<Entity> records)
			throws ConstraintException {
		List<Object> current = new ArrayList<Object>(functions.size());
		List<Object> other = new ArrayList<Object>(functions.size());

		for (int i = 0; i < records.size(); i++) {
			current.clear();

			for (Function a : functions) {
				current.add(records.get(i).getValue(a));
			}

			if (i + 1 < records.size()) {
				for (int j = i + 1; j < records.size(); j++) {
					other.clear();

					for (Function a : functions) {
						other.add(records.get(j).getValue(a));
					}

					if (current.equals(other)) {
						throw new ConstraintException(this, j);
					}
				}
			}
		}
	}

	/**
	 * Validates all the entities which are currently in the associated entity
	 * type according to the specified constraint type.
	 *
	 * @throws edu.napier.soc.xfdm.model.ConstraintException
	 *          if not all the entities are valid.
	 */
	public void validateValues() throws ConstraintException {
		List<Entity> records = entityType.getEntities();

		switch (constraintType) {
			case UNIQUE:
				validateUnique(records);
				break;
			case TOTAL:
				validateTotal(records);
				break;
		}
	}

	private void validateFixed(Entity e) throws ConstraintException {
		for (Function f : functions) {
			if (e.getValue(f) != null) {
				throw new ConstraintException(this, 0);
			}
		}
	}

	/**
	 * Validates {@code e} according to the constraint type of this
	 * {@code Constraint}.
	 *
	 * @param e the entity to validate.
	 * @throws edu.napier.soc.xfdm.model.ConstraintException
	 *          if {@code e} is not valid.
	 */
	public void validateValue(Entity e) throws ConstraintException {
		List<Entity> records = entityType.getEntities();

		switch (constraintType) {
			case UNIQUE:
				validateUnique(e, records);
				break;
			case TOTAL:
				validateTotal(e);
				break;
			case FIXED:
				validateFixed(e);
				break;
		}
	}

	private void validateUnique(Entity e, List<Entity> records)
			throws ConstraintException {
		List<Object> values = new LinkedList<Object>();
		for (Function f : functions) {
			values.add(e.getValue(f));
		}
		validateUnique(values, records);
	}

	private void validateUnique(List<Object> current, List<Entity> records)
			throws ConstraintException {
		if (current.size() != functions.size()) {
			throw new IllegalArgumentException(
					"values must be size of functions.");
		}
		List<Object> other = new ArrayList<Object>(functions.size());

		for (Entity e1 : records) {
			other.clear();

			for (Function f : functions) {
				other.add(e1.getValue(f));
			}

			if (current.equals(other)) {
				throw new ConstraintException(this, 0);
			}
		}
	}

	/**
	 * @return the constraint type of this [@code Constraint}.
	 */
	public ConstraintType getConstraintType() {
		return constraintType;
	}

	/**
	 * @return the name of this {@code Constraint}.
	 */
	public String getName() {
		return name;
	}

	@Override
	public boolean equals(Object that) {
		if (this == that) {
			return true;
		}

		if (!(that instanceof Constraint)) {
			return false;
		}

		Constraint c = (Constraint) that;

		return c.name.equalsIgnoreCase(this.name);
	}

	@Override
	public int hashCode() {
		return name.toLowerCase().hashCode();
	}
}